﻿namespace EPiServer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Core;
    using DataAbstraction;
    using Filters;
    using PageTypeBuilder;

    /// <summary>
    /// Provides a Linq friendly querable interface on top of the FindPagesWithCriteria methods.
    /// </summary>
    /// <typeparam name="T">The PageData object type.  This could be a PageData object or another type deriving form PageData such as
    /// a page type builder class.</typeparam>
    public class FindAllPagesWithCriteriaContext<T> : FindPagesWithCriteriaContext<T>
        where T : PageData, new()
    {
        #region Constructors

        public FindAllPagesWithCriteriaContext(PageReference startPage) 
            : this(startPage, null, null)
        {
        }

        public FindAllPagesWithCriteriaContext(PageReference startPage, string languageBranch) 
            : this(startPage, languageBranch, null)
        {
        }

        public FindAllPagesWithCriteriaContext(PageReference startPage, string languageBranch, ILanguageSelector languageSelector)
            : base(startPage, languageBranch, languageSelector)
        {
            IgnoreAccessRightsAndPermissions = true;
        }

        #endregion Constructors
    }

    /// <summary>
    /// Provides a Linq friendly querable interface on top of the FindPagesWithCriteria methods.
    /// </summary>
    /// <typeparam name="T">The PageData object type.  This could be a PageData object or another type deriving form PageData such as
    /// a page type builder class.</typeparam>
    public class FindPagesWithCriteriaContext<T> : IOrderedQueryable<T>, IQueryProvider 
        where T : PageData, new()
    {

        #region Members

        private readonly PropertyCriteriaCollection _propertyCriteriaCollection = new PropertyCriteriaCollection();
        private readonly PageReference _startPage;
        private readonly string _languageBranch;
        private readonly ILanguageSelector _languageSelector;
        private Expression _expression;
        private IList<PropertyInfo> _properties;
        private bool _and = true;
        private bool _not;
        private readonly List<KeyValuePair<string, bool>> _sortOrder = new List<KeyValuePair<string, bool>>();
        protected bool IgnoreAccessRightsAndPermissions;

        #endregion Members

        #region Constructors

        public FindPagesWithCriteriaContext(PageReference startPage) 
            : this(startPage, null, null)
        {
        }

        public FindPagesWithCriteriaContext(PageReference startPage, string languageBranch) 
            : this(startPage, languageBranch, null)
        {
        }

        public FindPagesWithCriteriaContext(PageReference startPage, string languageBranch, ILanguageSelector languageSelector)
        {
            _startPage = startPage;
            _languageBranch = languageBranch;
            _languageSelector = languageSelector;
        }

        #endregion Constructors

        #region IQueryable Members

        Type IQueryable.ElementType
        {
            get { return typeof(T); }
        }

        Expression IQueryable.Expression
        {
            get { return Expression.Constant(this); }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return this; }
        }

        #endregion IQueryable Members

        #region IEnumerable Members

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return (this as IQueryable).Provider.Execute<IEnumerator<T>>(_expression);
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return (this as IQueryable).GetEnumerator();
        }

        private void ProcessExpression(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Equal)
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.Equal);

            if (expression.NodeType == ExpressionType.MemberAccess)
                ProcessEqualityResultMemberAccess((MemberExpression)expression, CompareCondition.Equal);

            if (expression.NodeType == ExpressionType.NotEqual)
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.NotEqual);

            if (expression.NodeType == ExpressionType.LessThan)
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.LessThan);

            if (expression.NodeType == ExpressionType.LessThanOrEqual)
            {
                bool oldAnd = _and;
                _and = false;
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.LessThan);
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.Equal);
                _and = oldAnd;
            }

            if (expression.NodeType == ExpressionType.GreaterThan)
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.GreaterThan);

            if (expression.NodeType == ExpressionType.GreaterThanOrEqual)
            {
                bool oldAnd = _and;
                _and = false;
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.GreaterThan);
                ProcessEqualiltyResult((BinaryExpression)expression, CompareCondition.Equal);
                _and = oldAnd;
            }

            if (expression.NodeType == ExpressionType.AndAlso)
            {
                _and = true;
                BinaryExpression binaryExpression = expression as BinaryExpression;
                ProcessExpression(binaryExpression.Left);
                ProcessExpression(binaryExpression.Right);
            }

            if (expression.NodeType == ExpressionType.OrElse)
            {
                _and = false;
                BinaryExpression binaryExpression = expression as BinaryExpression;
                ProcessExpression(binaryExpression.Left);
                ProcessExpression(binaryExpression.Right);
            }

            if (expression.NodeType == ExpressionType.Not)
                _not = true;

            if (expression.NodeType == ExpressionType.Call)
                ProcessMethodCallExpression(expression as MethodCallExpression);

            if (expression is UnaryExpression)
            {
                UnaryExpression uExp = expression as UnaryExpression;
                ProcessExpression(uExp.Operand);
                _not = false;
            }
            else if (expression is LambdaExpression)
                ProcessExpression(((LambdaExpression)expression).Body);
        }

        private void ProcessEqualiltyResult(BinaryExpression expression, CompareCondition compareCondition)
        {
            MemberExpression leftExpression = expression.Left as MemberExpression;
            string propertyName = GetPropertyName(leftExpression);
            PropertyDataType propertyDataType = GetPropertyDataType(propertyName);
            propertyName = GetPropertyName(propertyName);

            if (propertyDataType == PropertyDataType.Category)
            {
                CategoryList categoryList = GetValue<CategoryList>(propertyName, expression);

                foreach (int category in categoryList)
                {
                    bool required = (_and && categoryList.Count == 1);
                    AddCriteria(propertyName, compareCondition, required, propertyDataType, Category.Find(category).Name, false);
                }
            }
            else
            {
                string value = GetValue<string>(propertyName, expression);
                AddCriteria(propertyName, compareCondition, _and, propertyDataType, value, value == null);
            }
        }

        private void ProcessEqualityResultMemberAccess(MemberExpression expression, CompareCondition compareCondition)
        {
            string propertyName = GetPropertyName(expression);
            PropertyDataType propertyDataType = GetPropertyDataType(propertyName);
            string value = _not ? false.ToString() : true.ToString();
            propertyName = GetPropertyName(propertyName);
            AddCriteria(propertyName, compareCondition, _and, propertyDataType, value, false);
        }

        private void ProcessMethodCallExpression(MethodCallExpression methodCallExpression)
        {
            MemberExpression memberExpression = methodCallExpression.Object as MemberExpression;
            string propertyName = GetPropertyName(memberExpression);
            PropertyDataType propertyDataType = GetPropertyDataType(propertyName);
            propertyName = GetPropertyName(propertyName);
            string value;

            switch (methodCallExpression.Arguments[0].NodeType)
            {
                case ExpressionType.MemberAccess:
                {
                    MemberExpression expression = methodCallExpression.Arguments[0] as MemberExpression;
                    value = (string) Expression.Lambda(expression).Compile().DynamicInvoke();
                }
                break;
                case ExpressionType.Call:
                {
                    MethodCallExpression expression = methodCallExpression.Arguments[0] as MethodCallExpression;
                    value = (string)Expression.Lambda(expression).Compile().DynamicInvoke();
                }
                break;
                default:
                    value = methodCallExpression.Arguments[0].ToString().Substring(1);
                    value = value.Substring(0, value.Length - 1);
                    break;
            }

            if (string.Equals(methodCallExpression.Method.Name, "StartsWith"))
                AddCriteria(propertyName, CompareCondition.StartsWith, _and, propertyDataType, value, false);
            else if (string.Equals(methodCallExpression.Method.Name, "Contains"))
                AddCriteria(propertyName, CompareCondition.Contained, _and, propertyDataType, value, false);
            else
                throw new Exception("FindPagesWithCriteriaContext: StartsWith and Contains are the only methods allowed to be called on a string object.");
        }

        private string GetPropertyName(MemberExpression memberExpression)
        {
            return memberExpression.Member.Name;
        }

        private string GetPropertyName(string propertyName)
        {
            Dictionary<string, string> properties = new Dictionary<string, string>();
            properties.Add("LinkURL", "PageLinkURL");
            properties.Add("LinkType", "PageShortcutType");
            properties.Add("URLSegment", "PageURLSegement");
            properties.Add("PageGuid", "PageGUID");
            properties.Add("ParentLink", "PageParentLink");
            properties.Add("ArchiveLink", "PageArchiveLink");
            properties.Add("IsDeleted", "PageDeleted");
            properties.Add("Created", "PageCreated");
            properties.Add("LanguageID", "PageLanguageBranch");
            properties.Add("CreatedBy", "PageCreatedBy");
            properties.Add("Changed", "PageChanged");
            properties.Add("Saved", "PageSaved");
            properties.Add("StartPublish", "PageStartPublish");
            properties.Add("StopPublish", "PageStopPublish");
            properties.Add("VisibleInMenu", "PageVisibleInMenu");
            properties.Add("MasterLanguageBranch", "PageMasterLanguageBranch");
            properties.Add("LanguageBranch", "PageLanguageBranch");
            properties.Add("Category", "PageCategory");

            return properties.ContainsKey(propertyName) ? properties[propertyName] : propertyName;
        }

        private void AddCriteria(string propertyName, CompareCondition compareCondition, bool required,
            PropertyDataType propertyDataType, string value, bool isNull)
        {
            _propertyCriteriaCollection.Add(new PropertyCriteria
            {
                Name = propertyName,
                Condition = compareCondition,
                IsNull = isNull,
                Required = required,
                Type = propertyDataType,
                Value = value
            });
        }

        private T GetValue<T>(string propertyName, BinaryExpression expression)
        {
            object value = null;

            if (expression.Right.NodeType == ExpressionType.Constant)
            {
                value = ((ConstantExpression)expression.Right).Value;
            }

            if (expression.Right.NodeType == ExpressionType.MemberAccess)
                value = Expression.Lambda(expression.Right as MemberExpression).Compile().DynamicInvoke();

            if (expression.Right.NodeType == ExpressionType.Call)
            {
                MethodCallExpression methodCallExpression = expression.Right as MethodCallExpression;
                value = Expression.Lambda(methodCallExpression).Compile().DynamicInvoke();
            }

            if (propertyName == "PageTypeName")
                value = PageType.Load(value.ToString()).ID;

            if (typeof(T) == typeof(string) && value != null)
                value = value.ToString();

            return (T)value;
        }

        private PropertyDataType GetPropertyDataType(string propertyName)
        {
            PageTypePropertyAttribute pageTypePropertyAttribute = GetPageTypePropertyAttribute(propertyName);

            if (pageTypePropertyAttribute != null)
            {
                PropertyData instance = Activator.CreateInstance(pageTypePropertyAttribute.Type) as PropertyData;

                if (instance != null)
                    return instance.Type;
            }

            Type propertyType = _properties.Where(current => string.Equals(current.Name, propertyName)).First().PropertyType;

            if (propertyType == typeof(bool) || propertyType == typeof(bool?))
                return PropertyDataType.Boolean;

            if (propertyType == typeof(CategoryList))
                return PropertyDataType.Category;

            if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?))
                return PropertyDataType.Date;

            if (propertyType == typeof(float) || propertyType == typeof(float?) || 
                propertyType == typeof(decimal) || propertyType == typeof(decimal?) ||
                propertyType == typeof(double) || propertyType == typeof(double?))
            {
                return PropertyDataType.FloatNumber;
            }

            if (propertyType == typeof(string))
                return PropertyDataType.String;

            if (propertyType == typeof(short) || propertyType == typeof(short?) ||
                propertyType == typeof(int) || propertyType == typeof(int?) ||
                propertyType == typeof(long) || propertyType == typeof(long?))
            {
                return PropertyDataType.Number;
            }

            if (propertyType == typeof(PageReference))
                return PropertyDataType.PageReference;

            if (propertyName == "PageTypeName" || propertyName == "PageTypeID")
                return PropertyDataType.PageType;

            return PropertyDataType.String;
        }

        private PageTypePropertyAttribute GetPageTypePropertyAttribute(string propertyName)
        {
            return _properties.Where(current => string.Equals(current.Name, propertyName))
                .First()
                .GetCustomAttributes(typeof (PageTypePropertyAttribute), false)
                .FirstOrDefault() as PageTypePropertyAttribute;
        }

        #endregion IEnumerable Members

        #region IQueryProvider Members

        IQueryable<TS> IQueryProvider.CreateQuery<TS>(Expression expression)
        {
            if (typeof(TS) != typeof(T))
                throw new Exception("Only " + typeof(T).FullName + " objects are supported.");

            _expression = expression;

            MethodCallExpression methodCall = _expression as MethodCallExpression;

            if (methodCall.Method.Name == "Where")
            {
                if (_properties == null)
                    _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);

                _propertyCriteriaCollection.Clear();

                if (typeof(T).GetCustomAttributes(typeof(PageTypeAttribute), false).Count() > 0 && _propertyCriteriaCollection.Count == 0)
                    AddCriteria("PageTypeID", CompareCondition.Equal, true, PropertyDataType.PageType, PageTypeResolver.Instance.GetPageTypeID(typeof(T)).Value.ToString(), false);
            }

            if (methodCall.Method.ReturnType.Name.Equals(typeof(IOrderedQueryable<T>).Name))
            {
                bool ascending = !methodCall.Method.Name.Contains("Descending");
                UnaryExpression unaryExpression = methodCall.Arguments[1] as UnaryExpression;
                LambdaExpression lambdaExpression = unaryExpression.Operand as LambdaExpression;
                MemberExpression memberExpression = lambdaExpression.Body as MemberExpression;
                string propertyName = memberExpression.Member.Name;
                _sortOrder.Add(new KeyValuePair<string, bool>(propertyName, ascending));
            }
            else
            {
                foreach (var param in methodCall.Arguments)
                    ProcessExpression(param);

                WriteOutCriteria();
            }

            return (IQueryable<TS>)this;
        }

        IQueryable IQueryProvider.CreateQuery(Expression expression)
        {
            return (this as IQueryProvider).CreateQuery<T>(expression);
        }

        TResult IQueryProvider.Execute<TResult>(Expression expression)
        {
            IList<T> pages = GetResults();
            pages = SortPages(pages);
            return (TResult)pages.GetEnumerator();
        }

        private IList<T> GetResults()
        {
            IEnumerable<PageData> pages;

            ILanguageSelector languageSelector = _languageSelector;

            if (IgnoreAccessRightsAndPermissions && languageSelector == null)
                languageSelector = new LanguageSelector(DataFactory.Instance.GetPage(_startPage).MasterLanguageBranch);

            if (languageSelector == null)
            { 
                pages = IgnoreAccessRightsAndPermissions ? DataFactory.Instance.FindAllPagesWithCriteria(_startPage, _propertyCriteriaCollection, _languageBranch, null) 
                                                         : DataFactory.Instance.FindPagesWithCriteria(_startPage, _propertyCriteriaCollection, _languageBranch);
            }
            else
            {
                pages = IgnoreAccessRightsAndPermissions ? DataFactory.Instance.FindAllPagesWithCriteria(_startPage, _propertyCriteriaCollection, _languageBranch, languageSelector)
                                                         : DataFactory.Instance.FindPagesWithCriteria(_startPage, _propertyCriteriaCollection, _languageBranch, languageSelector);
            }

            return !string.Equals(typeof(T).FullName, typeof(PageData).FullName) ? pages.Select(page => PageTypeResolver.Instance.ConvertToTyped(page) as T).ToList() 
                                                                                 : pages.Select(current => current as T).ToList();
        }

        private IList<TPageData> SortPages<TPageData>(IList<TPageData> pages)
        {
            int counter = 0;

            IQueryable<TPageData> queryablePages = pages.AsQueryable();
            IOrderedQueryable<TPageData> orderedQueryablePages = null;

            foreach (var sortOrder in _sortOrder)
            {
                counter++;
                string methodName = counter == 1 ? "OrderBy" : "ThenBy";
                methodName += sortOrder.Value ? string.Empty : "Descending";

                IQueryable<TPageData> o = orderedQueryablePages ?? queryablePages;
                orderedQueryablePages = ApplyOrder(o, sortOrder.Key, methodName);
            }

            return orderedQueryablePages != null ? orderedQueryablePages.ToList() : pages;
        }

        private void WriteOutCriteria()
        {
            Debug.WriteLine("FindPagesWithCriteriaContext criteria:");

            for (int i = 1; i <= _propertyCriteriaCollection.Count; i++)
            {
                PropertyCriteria criteria = _propertyCriteriaCollection[i - 1];
                Debug.WriteLine(string.Empty);
                Debug.WriteLine(string.Format("Critieria {0}:", i));
                Debug.WriteLine(string.Format("Name: {0}", criteria.Name));
                Debug.WriteLine(string.Format("Condition: {0}", criteria.Condition));
                Debug.WriteLine(string.Format("Required: {0}", criteria.Required));
                Debug.WriteLine(string.Format("Type: {0}", criteria.Type));
                Debug.WriteLine(string.Format("Value: {0}", criteria.Value));
                Debug.WriteLine(string.Format("IsNull: {0}", criteria.IsNull));
            }
        }

        private IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string propertyName, string methodName)
        {
            Type type = typeof(T);
            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;

            PropertyInfo propertyInfo = _properties.Where(current => string.Equals(current.Name, propertyName)).First();
            expr = Expression.Property(expr, propertyInfo);
            type = propertyInfo.PropertyType;

            Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
            LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);

            object result = typeof(Queryable).GetMethods().Single(
                    method => method.Name == methodName
                            && method.IsGenericMethodDefinition
                            && method.GetGenericArguments().Length == 2
                            && method.GetParameters().Length == 2)
                    .MakeGenericMethod(typeof(T), type)
                    .Invoke(null, new object[] { source, lambda });
               
            return (IOrderedQueryable<T>)result;
        }

        object IQueryProvider.Execute(Expression expression)
        {
            return (this as IQueryProvider).Execute<IEnumerator<T>>(expression);
        }

        #endregion IQueryProvider Members

    }
}
